# -*- coding: utf-8 -*-

import copy

from PyQt4.QtGui import *
from PyQt4.QtCore import *
from PyQt4.QtXml import *

from Tileset import *
from MapScene import *

class MapWidget(QGraphicsView):

	class PutEntityCommand(QUndoCommand):
		def __init__(self, map, zone_index, entity):
			QUndoCommand.__init__(self, "Put entity (%s)" % entity.id)
			self.map = map
			self.zone_index = zone_index
			self.entity = entity
			self.entity_index = -1

		def undo(self):
			self.map.go_to_zone(self.zone_index)
			self.map.delete_entity(self.entity_index)

		def redo(self):
			self.map.go_to_zone(self.zone_index)
			self.entity_index = self.map.add_entity(self.entity)

	class MoveEntityCommand(QUndoCommand):
		pass

	class DeleteEntityCommand(QUndoCommand):
		pass

	class PutTileCommand(QUndoCommand):
		##
		# @param map: MapWidget
		# @param zone_index: index of zone containing the modified tile
		# @param tile_index: index of tile position in the zone
		# @param new_id: tile_id after edition
		# @param old_id: tile_id before edition
		#
		def __init__(self, map, zone_index, tile_index, old_id, new_id):
			QUndoCommand.__init__(self, "Put tile (%d)" % new_id)
			self.map = map
			self.zone_index = zone_index
			self.tile_index = tile_index
			self.new_id = new_id
			self.old_id = old_id

		def undo(self):
			self.map.go_to_zone(self.zone_index)
			self.map.set_tile(self.tile_index, self.old_id)

		def redo(self):
			self.map.go_to_zone(self.zone_index)
			self.map.set_tile(self.tile_index, self.new_id)


	class FillZoneCommand(QUndoCommand):
		def __init__(self, map, zone_index, tile_id):
			QUndoCommand.__init__(self, "Fill zone (tile %d)" % tile_id)
			self.map = map
			self.zone_index = zone_index
			self.tile_id = tile_id
			self.tiles_before_filling = copy.copy(map.get_current_zone().tiles)

		def undo(self):
			self.map.go_to_zone(self.zone_index)
			index = 0
			while index < len(self.map.get_current_zone().tiles):
				self.map.set_tile(index, self.tiles_before_filling[index])
				index += 1

		def redo(self):
			self.map.go_to_zone(self.zone_index)
			index = 0
			while index < len(self.map.get_current_zone().tiles):
				self.map.set_tile(index, self.tile_id)
				index += 1

	class Zone:

		class Entity:
			def __init__(self, id, x, y):
				self.id = id
				self.x = x
				self.y = y

		def __init__(self, tiles=[]):
			self.tiles = tiles
			self.mobs = []

		def add_mob(self, id, x, y):
			self.mobs.append(MapWidget.Zone.Entity(id, x, y))

		def tiles_to_str(self):
			return " ".join(str(tile_id) for tile_id in self.tiles)

	def __init__(self):
		self.scene = MapScene()
		self.scene.setItemIndexMethod(QGraphicsScene.BspTreeIndex)
		QGraphicsView.__init__(self, self.scene)

		self.connect(self.scene, SIGNAL("tile_selected"), self.update_tile)
		self.connect(self.scene, SIGNAL("entity_added"), self.on_entity_added)
		self.connect(self.scene, SIGNAL("entity_moved"), self.on_entity_moved)
		self.connect(self.scene, SIGNAL("entity_deleted"), self.on_entity_deleted)


		# view properties
		self.setMouseTracking(True)
		self.setBackgroundBrush(QBrush(Qt.black, Qt.Dense7Pattern))

		self.tileset = None
		self.zones = []
		self.current_zone_index = -1

		self.map_width = 0
		self.map_height = 0
		self.zone_width = 0
		self.zone_height = 0
		self.grid_visible = False
		self.loaded = False

		# undo/redo
		self.undo_stack = QUndoStack()

	##
	# Set zone size, in tiles
	# @param width: nb cols of tiles
	# @param height: nb lines of tiles
	#
	def set_zone_size(self, width, height):
		self.zone_width = int(width)
		self.zone_height = int(height)
		self.scene.set_size(self.zone_width, self.zone_height)

	##
	# @return true if a map is opened
	#
	def is_loaded(self):
		return self.loaded

	##
	# Create an empty map filled with default tile
	#
	def create_empty_map(self, props):
		self.clear()

		self.tileset_filename = props["tileset"]
		self.set_zone_size(props["zone_width"], props["zone_height"])

		self.map_width = props["map_width"]
		self.map_height = props["map_height"]
		nb_zones = self.map_width * self.map_height
		nb_tiles = self.zone_width * self.zone_height
		for i in xrange(nb_zones):
			tiles = [0] * nb_tiles
			self.zones.append(MapWidget.Zone(tiles))

		print len(self.zones), "zones created"
		self.current_zone_index = 0
		self.scene.load_zone(self.zones[0], self.tileset)

		self.loaded = True
		self.scene.build_grid()
		self.scene.show_grid(self.grid_visible)


	##
	# Load a map zone from an XML document
	#
	def load_from_xml(self, document):
		self.clear()

		# parse root attributes
		root = document.documentElement()
		self.tileset_filename = root.attribute("tileset")
		self.map_width = int(root.attribute("width"))
		self.map_height = int(root.attribute("height"))

		zone_width = int(root.attribute("zone_width"))
		zone_height = int(root.attribute("zone_height"))
		self.set_zone_size(zone_width, zone_height)

		# load zones
		node = root.firstChildElement("zone")
		while not node.isNull():
			tiles = str(node.firstChildElement("tiles").text()).split()
			tiles = [int(tile_id) for tile_id in tiles]

			zone = MapWidget.Zone(tiles)
			node_mobs = node.firstChildElement("mobs")
			if node_mobs:
				node_mob = node_mobs.firstChildElement()
				while not node_mob.isNull():
					id = str(node_mob.attribute("id"))
					x = int(node_mob.attribute("x"))
					y = int(node_mob.attribute("y"))
					zone.add_mob(id, x, y)

					node_mob = node_mob.nextSiblingElement()

			self.zones.append(zone)
			node = node.nextSiblingElement("zone")

		print len(self.zones), "zones loaded"

		self.current_zone_index = 0
		self.scene.load_zone(self.zones[0], self.tileset)

		self.loaded = True
		self.scene.build_grid()
		self.scene.show_grid(self.grid_visible)


	def save_to_xml(self, filename):

		document = QDomDocument()
		header = document.createProcessingInstruction("xml", 'version="1.0" encoding="utf-8"')
		document.appendChild(header)

		# save map attributes
		root = document.createElement("map")
		root.setAttribute("tileset", str(self.tileset_filename))
		root.setAttribute("width", str(self.map_width))
		root.setAttribute("height", str(self.map_height))
		root.setAttribute("zone_width", str(self.zone_width))
		root.setAttribute("zone_height", str(self.zone_height))
		document.appendChild(root)

		# write zones
		for zone in self.zones:
			node_zone = document.createElement("zone")
			root.appendChild(node_zone)

			# tile IDs
			tiles = document.createElement("tiles")
			str_tiles = zone.tiles_to_str()
			tiles.appendChild(document.createTextNode(str_tiles))
			node_zone.appendChild(tiles)

			# mobs (if any)
			if zone.mobs:
				node_mobs = document.createElement("mobs")
				for mob in zone.mobs:
					node_mob = document.createElement("mob")
					node_mob.setAttribute("id", mob.id)
					node_mob.setAttribute("x", mob.x)
					node_mob.setAttribute("y", mob.y)
					node_mobs.appendChild(node_mob)
				node_zone.appendChild(node_mobs)

		# write XML to file
		content = document.toString()
		file = open(filename, "w")
		file.write(content)
		file.close()

	##
	# Export whole map to a single PNG file
	#
	def save_to_png(self, filename):

		px_width = self.map_width * self.zone_width * self.tileset.TILESIZE
		px_height = self.map_height * self.zone_height * self.tileset.TILESIZE
		image = QImage(px_width, px_height, QImage.Format_RGB888)

		painter = QPainter(image)
		for index_zone, zone in enumerate(self.zones):
			# compute zone offset
			x, y = self.index_to_pos(index_zone)
			x *= self.zone_width * self.tileset.TILESIZE
			y *= self.zone_height * self.tileset.TILESIZE
			for index_tile, tile_id in enumerate(zone.tiles):
				# compute tile position
				x_tile, y_tile = self.scene.index_to_pos(index_tile)
				tile_image = self.tileset.get_tile_by_id(tile_id)
				painter.drawImage(x + x_tile, y + y_tile, tile_image)
		painter.end()
		image.save(filename)

	##
	# Set tileset linked to this map, relative to its path
	#
	def set_tileset_filename(self, tileset_filename):
		self.tileset_filename = tileset_filename


	##
	# Reload tiles from tileset
	#
	def refresh_map(self):
		print "refresh map..."

		self.scene.refresh(self.tileset)
		self.scene.show_grid(self.grid_visible)


	def clear(self):
		self.scene.clear()
		del self.zones[:]
		self.undo_stack.clear()


	def update_tile(self, tile_index):
		tile_index -= 1
		selected_id = self.tileset.get_selected_tile_id()

		old_tile_id = self.get_current_zone().tiles[tile_index]
		# update only if new tile is a different one
		if selected_id != old_tile_id:
			#self.set_tile(tile_index, selected_id)
			command = MapWidget.PutTileCommand(self, self.current_zone_index, tile_index, old_tile_id, selected_id)
			self.undo_stack.push(command)

	##
	# Edit a tile in the current zone
	#
	def set_tile(self, tile_index, tile_id):
		# update scene
		tile_image = self.tileset.get_tile_by_id(tile_id)
		self.scene.set_tile(tile_index, tile_image, tile_id)
		# update model
		self.get_current_zone().tiles[tile_index] = tile_id



	def set_tileset(self, tileset):
		self.tileset = tileset
		# refresh map when a tileset is loaded
		self.connect(self.tileset, SIGNAL("tilesetLoaded()"), self.refresh_map)


	def toggle_grid(self, visible):
		self.scene.show_grid(visible)
		self.grid_visible = visible

	##
	# Paint the current zone with the selected tile in the opened tileset
	#
	def fill_with_active_tile(self):
		selected_id = self.tileset.get_selected_tile_id()
		command = MapWidget.FillZoneCommand(self, self.current_zone_index, selected_id)
		self.undo_stack.push(command)


	##
	# Get displayed zone
	#
	def get_current_zone(self):
		return self.zones[self.current_zone_index]

	##
	# Change current zone
	#
	def move_zone(self, dx, dy):
		if not self.is_loaded():
			return

		x, y = self.index_to_pos(self.current_zone_index)
		x += dx
		y += dy
		if 0 <= x < self.map_width and 0 <= y < self.map_height:
			index = self.pos_to_index(x, y)
			self.current_zone_index = index
			self.scene.load_zone(self.zones[index], self.tileset)

	##
	# Change current zone
	#
	def go_to_zone(self, index):
		if not self.is_loaded():
			return

		if self.is_loaded():
			if 0 <= index < len(self.zones) and self.current_zone_index != index:
				self.current_zone_index = index
				self.scene.load_zone(self.zones[index], self.tileset)

	##
	# 2D position => index in list
	#
	def pos_to_index(self, x, y):
		return y * self.map_width + x

	##
	# index in list => 2D position
	#
	def index_to_pos(self, index):
		x = index % self.map_width
		y = index / self.map_width
		return x, y

	def get_undo_stack(self):
		return self.undo_stack

	## Tab Entities callbacks

	def on_mob_selected(self, mob_id):
		self.scene.set_current_entity(mob_id)

	def on_decor_selected(self, decor_id):
		pass

	def on_item_selected(self, item_id):
		pass

	## Entity management callbacks ---------------------------------------------

	def on_entity_added(self, id, x, y):
		entity = MapWidget.Zone.Entity(id, x, y)
		command = MapWidget.PutEntityCommand(self, self.current_zone_index, entity)
		self.undo_stack.push(command)

	def add_entity(self, entity):
		zone = self.get_current_zone()
		zone.mobs.append(entity)
		self.scene.add_mob(entity)
		return len(zone.mobs) - 1

	def on_entity_moved(self):
		pass

	def on_entity_deleted(self):
		pass

	def delete_entity(self, index):
		zone = self.get_current_zone()
		del zone.mobs[index]
		self.scene.remove_mob_at(index)
